from pathlib import Path
from typing import Optional

import yaml
from kubernetes_asyncio import config, client


def format_selector(label_selector: dict = None, field_selector: dict = None) -> tuple:
    """
    格式化selector，返回label_selector和field_selector的字符串
    传入的label_selector和field_selector如下：
        label_selector = {
            "app": "test",
            "uid": "test"
        }
        field_selector = {
            "status.phase": "Running",
            "metadata.name": "test*"
        }
    """
    label_selector_str = None
    field_selector_str = None

    if label_selector:
        label_selector_str = ','.join([f"{k}={v}" for k, v in label_selector.items()])

    if field_selector:
        field_selector_str = ','.join([f"{k}={v}" for k, v in field_selector.items()])

    return label_selector_str, field_selector_str


def format_container_ports(ports: Optional[list[dict]] = None) -> list[client.V1ContainerPort]:
    """
    格式化container的ports
    传入的ports如下：
        [
            {
                "name": "test",
                "container_port": 80,
                "protocol": "TCP"
            }
        ]
    """
    container_ports = list()
    if ports:
        for index, port in enumerate(ports):
            container_ports.append(
                client.V1ContainerPort(
                    name=port.get('name', f'port-{index}'),
                    container_port=port.get('container_port'),
                    protocol=port.get('protocol', 'TCP')
                )
            )

    return container_ports


def format_container_env(envs: Optional[list[dict]]) -> list[client.V1EnvVar]:
    """
    格式化container的env
    传入的envs如下：
        [
            {
                "name": "test",
                "value": "test"
            }
        ]
    """
    container_env = list()
    if envs:
        for env in envs:
            container_env.append(
                client.V1EnvVar(
                    name=env.get('name'),
                    value=env.get('value')
                )
            )

    return container_env


def format_container_resources(resources: Optional[dict] = None, gpu_list: Optional[list[dict]] = None) -> client.V1ResourceRequirements:
    """
    格式化container的资源配置
    传入的resources如下：
        {
            "limits": {
                "cpu": "1",
                "memory": "1Gi"
            },
            "requests": {
                "cpu": "0.5",
                "memory": "500Mi"
            }
        }
    传入的gpu如下：
        [
            {
                "type": "bi-v100",
                "core": 100,
                "memory": 128
            }
        ]
    """
    if resources:
        limits = resources.get('limits') or dict()
        requests = resources.get('requests') or dict()
    else:
        limits = dict()
        requests = dict()

    if gpu_list:
        for gpu in gpu_list:
            # gpu类型，不传默认为bi-v100，全部转换为小写形式
            gpu_type = gpu.get('type', 'bi-v100').lower()

            gpu_core = gpu.get('core')
            gpu_memory = gpu.get('memory')

            match gpu_type:
                case 'bi-v100':
                    bi_v100_core = gpu_core or 100
                    # 如果没有指定memory，根据 gpu_core / 100 的比例，对应 memory 为 128 的倍数并取整
                    # 如果指定了memory，则使用指定的memory
                    bi_v100_memory = gpu_memory or (bi_v100_core * 128 / 100)

                    limits['iluvatar.ai/BI-V100.vCore'] = str(bi_v100_core)
                    limits['iluvatar.ai/BI-V100.vMem'] = str(bi_v100_memory)
                    requests['iluvatar.ai/BI-V100.vCore'] = str(bi_v100_core)
                    requests['iluvatar.ai/BI-V100.vMem'] = str(bi_v100_memory)
                case 'bi-v150':
                    bi_v150_core = gpu_core or 100
                    bi_v150_memory = gpu_memory or (bi_v150_core * 128 / 100)

                    # bi-v150是一卡双芯，所以资源是两倍的关系
                    bi_v150_core = str(bi_v150_core * 2)
                    bi_v150_memory = str(bi_v150_memory * 2)
                    limits['iluvatar.ai/BI-V150.vCore'] = bi_v150_core
                    limits['iluvatar.ai/BI-V150.vMem'] = bi_v150_memory
                    requests['iluvatar.ai/BI-V150.vCore'] = bi_v150_core
                    requests['iluvatar.ai/BI-V150.vMem'] = bi_v150_memory

                # 传入的gpu类型不在支持的列表中，目前先不处理，后续有需要再添加
                # case _:
                #     raise ValueError(f'gpu type {gpu_type} not supported')

    return client.V1ResourceRequirements(limits=limits, requests=requests)


def format_container_probe(probe: Optional[dict]) -> Optional[client.V1Probe]:
    """
    格式化container的probe
    传入的readiness_probe如下：
        {
            "type": "http_get",
            "path": "/",
            "port": 80,
            "scheme": "HTTP",

            # "type": "exec",
            # "command": ["ls"],
            #
            # "type": "tcp_socket",
            # "port": 80,

            "initial_delay_seconds": 0,
            "period_seconds": 10,
            "timeout_seconds": 1,
            "success_threshold": 1,
            "failure_threshold": 3
        }
    """
    if probe:
        initial_delay_seconds = probe.get('initial_delay_seconds', 0)
        period_seconds = probe.get('period_seconds', 10)
        timeout_seconds = probe.get('timeout_seconds', 1)
        success_threshold = probe.get('success_threshold', 1)
        failure_threshold = probe.get('failure_threshold', 3)

        readiness_probe_type = probe.get('type', 'http_get')
        match readiness_probe_type:
            case 'http_get':
                path = probe.get('path', '/')
                port = probe.get('port', 80)
                scheme = probe.get('scheme', 'HTTP')
                return client.V1Probe(
                    http_get=client.V1HTTPGetAction(path=path, port=port, scheme=scheme),
                    initial_delay_seconds=initial_delay_seconds,
                    period_seconds=period_seconds,
                    timeout_seconds=timeout_seconds,
                    success_threshold=success_threshold,
                    failure_threshold=failure_threshold
                )
            case 'exec':
                command = probe.get('command')
                if not command:
                    raise ValueError('exec command must exist')
                return client.V1Probe(
                    _exec=client.V1ExecAction(command=command),
                    initial_delay_seconds=initial_delay_seconds,
                    period_seconds=period_seconds,
                    timeout_seconds=timeout_seconds,
                    success_threshold=success_threshold,
                    failure_threshold=failure_threshold
                )
            case 'tcp_socket':
                port = probe.get('port', 80)
                return client.V1Probe(
                    tcp_socket=client.V1TCPSocketAction(port=port),
                    initial_delay_seconds=initial_delay_seconds,
                    period_seconds=period_seconds,
                    timeout_seconds=timeout_seconds,
                    success_threshold=success_threshold,
                    failure_threshold=failure_threshold
                )
    return None


def format_container_volume_mounts(volume_mounts: Optional[list[dict]]) -> list[client.V1VolumeMount]:
    container_volume_mounts = list()
    if volume_mounts:
        for volume_mount in volume_mounts:
            container_volume_mounts.append(
                client.V1VolumeMount(
                    name=volume_mount.get('name'),
                    mount_path=volume_mount.get('mount_path'),
                    read_only=volume_mount.get('read_only', False),
                    sub_path=volume_mount.get('sub_path'),
                    mount_propagation=volume_mount.get('mount_propagation', 'None'),
                    sub_path_expr=volume_mount.get('sub_path_expr')
                )
            )

    return container_volume_mounts


def format_volumes(volumes: dict[str, dict]) -> list[client.V1Volume]:
    format_volumes_data = list()
    for volume_name, volume in volumes.items():
        # 匹配volume的类型，创建对应的client.V1Volume
        volume_type = volume.get('type', 'persistent_volume_claim')
        match volume_type:
            case 'persistent_volume_claim':
                format_volumes_data.append(
                    client.V1Volume(
                        name=volume_name,
                        persistent_volume_claim=client.V1PersistentVolumeClaimVolumeSource(
                            claim_name=volume.get('persistent_volume_claim', dict()).get('claim_name')
                        )
                    )
                )
            case 'empty_dir':
                format_volumes_data.append(
                    client.V1Volume(
                        name=volume_name,
                        empty_dir=client.V1EmptyDirVolumeSource(
                            medium=volume.get('empty_dir', dict()).get('medium'),
                            size_limit=volume.get('empty_dir', dict()).get('size_limit')
                        )
                    )
                )
            case 'host_path':
                format_volumes_data.append(
                    client.V1Volume(
                        name=volume_name,
                        host_path=client.V1HostPathVolumeSource(
                            path=volume.get('host_path', dict()).get('path'),
                            type=volume.get('host_path', dict()).get('type')
                        )
                    )
                )
            case 'config_map':
                format_volumes_data.append(
                    client.V1Volume(
                        name=volume_name,
                        config_map=client.V1ConfigMapVolumeSource(
                            name=volume.get('config_map', dict()).get('name')
                        )
                    )
                )
            case 'secret':
                format_volumes_data.append(
                    client.V1Volume(
                        name=volume_name,
                        secret=client.V1SecretVolumeSource(
                            secret_name=volume.get('secret', dict()).get('secret_name')
                        )
                    )
                )
            case _:
                raise ValueError(f'volume type {volume.get("type")} not supported')
    return format_volumes_data


def format_containers_and_volumes(
        containers: list[dict],
        volumes: list[dict],
        init_containers: Optional[list[dict]] = None
) -> tuple[list[client.V1Container], list[client.V1Volume], list[client.V1Container]]:
    def format_containers(inner_containers: Optional[list[dict]], inner_volumes: list[dict], inner_volumes_dict: dict) -> list[client.V1Container]:
        """
        格式化containers，这里是为了避免重复代码，将init_containers和containers的格式化代码提取出来。
        inner_volumes_dict 是引用类型，修改后会影响外部的volumes_dict，不需要回传
        """
        format_inner_containers_data = list()
        if inner_containers:
            for container in inner_containers:
                # container的name和image必须存在，否则报错
                container_name = container.get('name')
                if not container_name:
                    raise ValueError('container name must exist')
                container_image = container.get('image')
                if not container_image:
                    raise ValueError('container image must exist')

                container_ports = format_container_ports(container.get('ports'))
                container_env = format_container_env(container.get('env'))
                container_resources = format_container_resources(container.get('resources'), container.get('gpu'))
                container_volume_mounts = format_container_volume_mounts(container.get('volume_mounts'))
                container_readiness_probe = format_container_probe(container.get('readiness_probe'))
                container_liveness_probe = format_container_probe(container.get('liveness_probe'))

                for volume_mount in container.get('volume_mounts', list()):
                    volume_name = volume_mount.get('name')

                    # 如果volume_name不在volumes_dict中，说明是新的volume，需要添加到volumes_dict中
                    if volume_name not in inner_volumes_dict:
                        # 先从volumes中找到对应的volume配置，然后添加到volumes_dict中。如果没有找到，就直接报错
                        for v in inner_volumes:
                            if v.get('name') == volume_name:
                                inner_volumes_dict[volume_name] = v
                                break
                        else:
                            raise ValueError(f'volume {volume_name} not found in volumes')

                format_inner_containers_data.append(
                    client.V1Container(
                        name=container_name,
                        image=container_image,
                        image_pull_policy=container.get('image_pull_policy', 'IfNotPresent'),
                        command=container.get('command'),
                        args=container.get('args'),
                        ports=container_ports,
                        env=container_env,
                        resources=container_resources,
                        volume_mounts=container_volume_mounts,
                        readiness_probe=container_readiness_probe,
                        liveness_probe=container_liveness_probe
                    )
                )
        return format_inner_containers_data

    volumes_dict = dict()
    format_containers_data = format_containers(containers, volumes, volumes_dict)
    format_init_containers_data = format_containers(init_containers, volumes, volumes_dict)
    format_volumes_data = format_volumes(volumes_dict)
    return format_containers_data, format_volumes_data, format_init_containers_data


class KubeBase:
    _api_client = None
    _app_v1_api = None
    _batch_v1_api = None
    _core_v1_api = None
    _networking_v1_api = None
    _is_loaded = False
    conf = None

    def __init__(self, kube_config: Optional[str] = None, kube_config_path: Optional[str | Path] = None):
        # 读取kube config
        if kube_config:
            self.conf = yaml.safe_load(kube_config)
        elif kube_config_path:
            with open(kube_config_path, 'r') as f:
                self.conf = yaml.safe_load(f)
        else:
            raise ValueError("kube_config or kube_config_path must exist at least one")

    async def load(self):
        # 加载kube config，之后的操作都会替换为原生库
        await config.load_kube_config_from_dict(self.conf)
        self._is_loaded = True

    @property
    def api_client(self):
        if self._api_client is None:
            self._api_client = client.ApiClient()
        return self._api_client

    @property
    def app_v1_api(self):
        if self._app_v1_api is None:
            self._app_v1_api = client.AppsV1Api(api_client=self.api_client)
        return self._app_v1_api

    @property
    def batch_v1_api(self):
        if self._batch_v1_api is None:
            self._batch_v1_api = client.BatchV1Api(api_client=self.api_client)
        return self._batch_v1_api

    @property
    def core_v1_api(self):
        if self._core_v1_api is None:
            self._core_v1_api = client.CoreV1Api(api_client=self.api_client)
        return self._core_v1_api

    @property
    def networking_v1_api(self):
        if self._networking_v1_api is None:
            self._networking_v1_api = client.NetworkingV1Api(api_client=self.api_client)
        return self._networking_v1_api
